//--------------------------------------------------------------------------------------
// File: SoundEffect.cpp
//
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// http://go.microsoft.com/fwlink/?LinkId=248929
//--------------------------------------------------------------------------------------

#include "pch.h"
#include "WAVFileReader.h"
#include "SoundCommon.h"

#include <list>

#if defined(_XBOX_ONE) && defined(_TITLE)
#include <apu.h>
#endif

using namespace DirectX;


//======================================================================================
// SoundEffect
//======================================================================================

// Internal object implementation class.
class SoundEffect::Impl : public IVoiceNotify
{
public:
    explicit Impl( _In_ AudioEngine* engine ) :
        mWaveFormat( nullptr ),
        mStartAudio( nullptr ),
        mAudioBytes( 0 ),
        mLoopStart( 0 ),
        mLoopLength( 0 ),
        mEngine( engine ),
        mOneShots( 0 )
#if defined(_XBOX_ONE) || (_WIN32_WINNT < _WIN32_WINNT_WIN8)
        , mSeekCount( 0 )
        , mSeekTable( nullptr )
#endif
#if defined(_XBOX_ONE) && defined(_TITLE)
        , mXMAMemory( nullptr )
#endif
    {
        assert( mEngine != 0 );
        mEngine->RegisterNotify( this, false );
    }

    ~Impl()
    {
        if ( !mInstances.empty() )
        {
            DebugTrace( "WARNING: Destroying SoundEffect with %Iu outstanding SoundEffectInstances\n", mInstances.size() );

            for( auto it = mInstances.begin(); it != mInstances.end(); ++it )
            {
                assert( *it != 0 );
                (*it)->OnDestroyParent();
            }

            mInstances.clear();
        }

        if ( mOneShots > 0 )
        {
            DebugTrace( "WARNING: Destroying SoundEffect with %u outstanding one shot effects\n", mOneShots );
        }

        if ( mEngine )
        {
            mEngine->UnregisterNotify( this, true, false );
            mEngine = nullptr;
        }

#if defined(_XBOX_ONE) && defined(_TITLE)
        if ( mXMAMemory )
        {
            ApuFree( mXMAMemory );
            mXMAMemory = nullptr;
        }
#endif
    }

    HRESULT Initialize( _In_ AudioEngine* engine, _Inout_ std::unique_ptr<uint8_t[]>& wavData,
                        _In_ const WAVEFORMATEX* wfx, _In_reads_bytes_(audioBytes) const uint8_t* startAudio, size_t audioBytes,
#if defined(_XBOX_ONE) || (_WIN32_WINNT < _WIN32_WINNT_WIN8)
                        _In_reads_opt_(seekCount) const uint32_t* seekTable, size_t seekCount,
#endif
                        uint32_t loopStart, uint32_t loopLength );

    void Play();

    // IVoiceNotify
    virtual void OnBufferEnd() override
    {
        InterlockedDecrement( &mOneShots );
    }

    virtual void OnCriticalError() override
    {
        mOneShots = 0;
    }

    virtual void OnReset() override
    {
        // No action required
    }

    virtual void OnUpdate() override
    {
        // We do not register for update notification
        assert(false);
    }

    virtual void OnDestroyEngine() override
    {
        mEngine = nullptr;
        mOneShots = 0;
    }

    virtual void OnTrim() override
    {
        // No action required
    }

    virtual void GatherStatistics( AudioStatistics& stats ) const override
    {
        stats.playingOneShots += mOneShots;
        stats.audioBytes += mAudioBytes;

#if defined(_XBOX_ONE) && defined(_TITLE)
        if ( mXMAMemory )
            stats.xmaAudioBytes += mAudioBytes;
#endif
    }

    const WAVEFORMATEX*                 mWaveFormat;
    const uint8_t*                      mStartAudio;
    uint32_t                            mAudioBytes;
    uint32_t                            mLoopStart;
    uint32_t                            mLoopLength;
    AudioEngine*                        mEngine;
    std::list<SoundEffectInstance*>     mInstances;
    uint32_t                            mOneShots;

#if defined(_XBOX_ONE) || (_WIN32_WINNT < _WIN32_WINNT_WIN8)
    uint32_t                            mSeekCount;
    const uint32_t*                     mSeekTable;
#endif

private:
    std::unique_ptr<uint8_t[]>          mWavData;

#if defined(_XBOX_ONE) && defined(_TITLE)
    void*                               mXMAMemory;
#endif
};


_Use_decl_annotations_
HRESULT SoundEffect::Impl::Initialize( AudioEngine* engine, std::unique_ptr<uint8_t[]>& wavData,
                                       const WAVEFORMATEX* wfx, const uint8_t* startAudio, size_t audioBytes,
#if defined(_XBOX_ONE) || (_WIN32_WINNT < _WIN32_WINNT_WIN8)
                                       const uint32_t* seekTable, size_t seekCount,
#endif
                                       uint32_t loopStart, uint32_t loopLength )
{
    if ( !engine || !IsValid( wfx ) || !startAudio || !audioBytes || !wavData )
        return E_INVALIDARG;

#ifdef _M_X64
    if ( audioBytes > 0xFFFFFFFF )
        return E_INVALIDARG;
#endif

    switch( GetFormatTag( wfx ) )
    {
    case WAVE_FORMAT_PCM:
    case WAVE_FORMAT_IEEE_FLOAT:
    case WAVE_FORMAT_ADPCM:
        // Take ownership of the buffer
        mWavData.reset( wavData.release() );

        // WARNING: We assume the wfx and startAudio parameters are pointers into the wavData memory buffer
        mWaveFormat = wfx;
        mStartAudio = startAudio;
        break;

#if defined(_XBOX_ONE) || (_WIN32_WINNT < _WIN32_WINNT_WIN8)

    case WAVE_FORMAT_WMAUDIO2:
    case WAVE_FORMAT_WMAUDIO3:
        if ( !seekCount || !seekTable )
        {
            DebugTrace( "ERROR: SoundEffect format xWMA requires seek table\n" );
            return E_FAIL;
        }

#ifdef _M_X64
        if ( seekCount > 0xFFFFFFFF )
            return E_INVALIDARG;
#endif

        // Take ownership of the buffer
        mWavData.reset( wavData.release() );

        // WARNING: We assume the wfx, startAudio, and mSeekTable parameters are pointers into the wavData memory buffer
        mWaveFormat = wfx;
        mStartAudio = startAudio;
        mSeekCount = static_cast<uint32_t>( seekCount );
        mSeekTable = seekTable;
        break;

#endif // _XBOX_ONE || _WIN32_WINNT < _WIN32_WINNT_WIN8

#if defined(_XBOX_ONE) && defined(_TITLE)

    case WAVE_FORMAT_XMA2:
        if ( !seekCount || !seekTable )
        {
            DebugTrace( "ERROR: SoundEffect format XMA2 requires seek table\n" );
            return E_FAIL;
        }

#ifdef _M_X64
        if ( seekCount > 0xFFFFFFFF )
            return E_INVALIDARG;
#endif

        {
            HRESULT hr = ApuAlloc( &mXMAMemory, nullptr,
                                   static_cast<UINT32>( audioBytes ), SHAPE_XMA_INPUT_BUFFER_ALIGNMENT );
            if ( FAILED(hr) )
            {
                DebugTrace( "ERROR: ApuAlloc failed. Did you allocate a large enough heap with ApuCreateHeap for all your XMA wave data?\n" );
                return hr;
            }
        }

        memcpy( mXMAMemory, startAudio, audioBytes );
        mStartAudio = reinterpret_cast<const uint8_t*>( mXMAMemory );

        mWavData.reset( new uint8_t[ sizeof(XMA2WAVEFORMATEX) + ( seekCount * sizeof(uint32_t) ) ] );

        memcpy( mWavData.get(), wfx, sizeof(XMA2WAVEFORMATEX) );
        mWaveFormat = reinterpret_cast<WAVEFORMATEX*>( mWavData.get() );

        // XMA seek table is Big-Endian
        {
            auto dest = reinterpret_cast<uint32_t*>( mWavData.get() + sizeof(XMA2WAVEFORMATEX) );
            for( size_t k = 0; k < seekCount; ++k )
            {
                dest[ k ] = _byteswap_ulong( seekTable[ k ]) ;
            }
        }

        mSeekCount = static_cast<uint32_t>( seekCount );
        mSeekTable = reinterpret_cast<const uint32_t*>( mWavData.get() + sizeof(XMA2WAVEFORMATEX) );

        wavData.reset();
        break;

#endif // _XBOX_ONE && _TITLE

    default:
        {
            DebugTrace( "ERROR: SoundEffect encountered an unsupported format tag (%u)\n", wfx->wFormatTag );
            return HRESULT_FROM_WIN32( ERROR_NOT_SUPPORTED );
        }
    }

    mAudioBytes = static_cast<uint32_t>( audioBytes );
    mLoopStart = loopStart;
    mLoopLength = loopLength;

    return S_OK;
}


void SoundEffect::Impl::Play()
{
    IXAudio2SourceVoice* voice = nullptr;
    mEngine->AllocateVoice( mWaveFormat, SoundEffectInstance_Default, true, &voice );
    
    if ( !voice )
        return;

    HRESULT hr = voice->Start( 0 );
    ThrowIfFailed( hr );

    XAUDIO2_BUFFER buffer;
    memset( &buffer, 0, sizeof(buffer) );
    buffer.AudioBytes = mAudioBytes;
    buffer.pAudioData = mStartAudio;
    buffer.Flags = XAUDIO2_END_OF_STREAM;
    buffer.pContext = this;

#if defined(_XBOX_ONE) || (_WIN32_WINNT < _WIN32_WINNT_WIN8)

    uint32_t tag = GetFormatTag( mWaveFormat );
    if ( tag == WAVE_FORMAT_WMAUDIO2 || tag == WAVE_FORMAT_WMAUDIO3 )
    {
        XAUDIO2_BUFFER_WMA wmaBuffer;
        memset( &wmaBuffer, 0, sizeof(wmaBuffer) );

        wmaBuffer.PacketCount = mSeekCount;
        wmaBuffer.pDecodedPacketCumulativeBytes = mSeekTable;

        hr = voice->SubmitSourceBuffer( &buffer, &wmaBuffer );
    }
    else
#endif
    {
        hr = voice->SubmitSourceBuffer( &buffer, nullptr );
    }
    if ( FAILED(hr) )
    {
        DebugTrace( "ERROR: SoundEffect failed (%08X) when submitting buffer:\n", hr );
        DebugTrace( "\tFormat Tag %u, %u channels, %u-bit, %u Hz, %u bytes\n", mWaveFormat->wFormatTag, 
                    mWaveFormat->nChannels, mWaveFormat->wBitsPerSample, mWaveFormat->nSamplesPerSec, mAudioBytes );
        throw std::exception( "SubmitSourceBuffer" );
    }

    InterlockedIncrement( &mOneShots );
}


//--------------------------------------------------------------------------------------
// SoundEffect
//--------------------------------------------------------------------------------------

// Public constructors.
_Use_decl_annotations_
SoundEffect::SoundEffect( AudioEngine* engine, const wchar_t* waveFileName )
  : pImpl(new Impl(engine) )
{
    WAVData wavInfo;
    std::unique_ptr<uint8_t[]> wavData;
    HRESULT hr = LoadWAVAudioFromFileEx( waveFileName, wavData, wavInfo );
    if ( FAILED(hr) )
    {
        DebugTrace( "ERROR: SoundEffect failed (%08X) to load from .wav file \"%S\"\n", hr, waveFileName );
        throw std::exception( "SoundEffect" );
    }

#if defined(_XBOX_ONE) || (_WIN32_WINNT < _WIN32_WINNT_WIN8)
    hr = pImpl->Initialize( engine, wavData, wavInfo.wfx, wavInfo.startAudio, wavInfo.audioBytes,
                            wavInfo.seek, wavInfo.seekCount,
                            wavInfo.loopStart, wavInfo.loopLength );
#else
    hr = pImpl->Initialize( engine, wavData, wavInfo.wfx, wavInfo.startAudio, wavInfo.audioBytes,
                            wavInfo.loopStart, wavInfo.loopLength );
#endif

    if ( FAILED(hr) )
    {
        DebugTrace( "ERROR: SoundEffect failed (%08X) to intialize from .wav file \"%S\"\n", hr, waveFileName );
        throw std::exception( "SoundEffect" );
    }
}


_Use_decl_annotations_
SoundEffect::SoundEffect( AudioEngine* engine, std::unique_ptr<uint8_t[]>& wavData,
                          const WAVEFORMATEX* wfx, const uint8_t* startAudio, size_t audioBytes )
  : pImpl(new Impl(engine) )
{
#if defined(_XBOX_ONE) || (_WIN32_WINNT < _WIN32_WINNT_WIN8)
    HRESULT hr = pImpl->Initialize( engine, wavData, wfx, startAudio, audioBytes, nullptr, 0, 0, 0 );
#else
    HRESULT hr = pImpl->Initialize( engine, wavData, wfx, startAudio, audioBytes, 0, 0 );
#endif
    if ( FAILED(hr) )
    {
        DebugTrace( "ERROR: SoundEffect failed (%08X) to intialize\n", hr );
        throw std::exception( "SoundEffect" );
    }
}


_Use_decl_annotations_
SoundEffect::SoundEffect( AudioEngine* engine, std::unique_ptr<uint8_t[]>& wavData,
                          const WAVEFORMATEX* wfx, const uint8_t* startAudio, size_t audioBytes,
                          uint32_t loopStart, uint32_t loopLength )
  : pImpl(new Impl(engine) )
{
#if defined(_XBOX_ONE) || (_WIN32_WINNT < _WIN32_WINNT_WIN8)
    HRESULT hr = pImpl->Initialize( engine, wavData, wfx, startAudio, audioBytes, nullptr, 0, loopStart, loopLength );
#else
    HRESULT hr = pImpl->Initialize( engine, wavData, wfx, startAudio, audioBytes, loopStart, loopLength );
#endif
    if ( FAILED(hr) )
    {
        DebugTrace( "ERROR: SoundEffect failed (%08X) to intialize\n", hr );
        throw std::exception( "SoundEffect" );
    }
}


#if defined(_XBOX_ONE) || (_WIN32_WINNT < _WIN32_WINNT_WIN8)

_Use_decl_annotations_
SoundEffect::SoundEffect( AudioEngine* engine, std::unique_ptr<uint8_t[]>& wavData,
                          const WAVEFORMATEX* wfx, const uint8_t* startAudio, size_t audioBytes,
                          const uint32_t* seekTable, size_t seekCount )
{
    HRESULT hr = pImpl->Initialize( engine, wavData, wfx, startAudio, audioBytes, seekTable, seekCount, 0, 0 );
    if ( FAILED(hr) )
    {
        DebugTrace( "ERROR: SoundEffect failed (%08X) to intialize\n", hr );
        throw std::exception( "SoundEffect" );
    }
}

#endif


// Move constructor.
SoundEffect::SoundEffect(SoundEffect&& moveFrom)
  : pImpl(std::move(moveFrom.pImpl))
{
}


// Move assignment.
SoundEffect& SoundEffect::operator= (SoundEffect&& moveFrom)
{
    pImpl = std::move(moveFrom.pImpl);
    return *this;
}


// Public destructor.
SoundEffect::~SoundEffect()
{
}


// Public methods.
void SoundEffect::Play()
{
    pImpl->Play();
}


std::unique_ptr<SoundEffectInstance> SoundEffect::CreateInstance( SOUND_EFFECT_INSTANCE_FLAGS flags )
{
    auto effect = new SoundEffectInstance( pImpl->mEngine, this, flags );
    assert( effect != 0 );
    pImpl->mInstances.emplace_back( effect );
    return std::unique_ptr<SoundEffectInstance>( effect );
}


void SoundEffect::UnregisterInstance( _In_ SoundEffectInstance* instance )
{
    auto it = std::find( pImpl->mInstances.begin(), pImpl->mInstances.end(), instance );
    if ( it == pImpl->mInstances.end() )
        return;

    pImpl->mInstances.erase( it );
}


// Public accessors.
bool SoundEffect::IsInUse() const
{
    return ( pImpl->mOneShots > 0 ) || !pImpl->mInstances.empty();
}


size_t SoundEffect::GetSampleSizeInBytes() const
{
    return pImpl->mAudioBytes;
}


size_t SoundEffect::GetSampleDuration() const
{
    if ( !pImpl->mWaveFormat || !pImpl->mWaveFormat->nChannels )
        return 0;

    switch( GetFormatTag( pImpl->mWaveFormat ) )
    {
    case WAVE_FORMAT_ADPCM:
        {
            auto adpcmFmt = reinterpret_cast<const ADPCMWAVEFORMAT*>( pImpl->mWaveFormat ); 

            uint64_t duration = uint64_t( pImpl->mAudioBytes / adpcmFmt->wfx.nBlockAlign ) * adpcmFmt->wSamplesPerBlock;
            int partial = pImpl->mAudioBytes % adpcmFmt->wfx.nBlockAlign;
            if ( partial )
            {
                if ( partial >= ( 7 * adpcmFmt->wfx.nChannels ) )
                    duration += ( partial * 2 / adpcmFmt->wfx.nChannels - 12 );
            }
            return static_cast<size_t>( duration );
        }

#if defined(_XBOX_ONE) || (_WIN32_WINNT < _WIN32_WINNT_WIN8)

    case WAVE_FORMAT_WMAUDIO2:
    case WAVE_FORMAT_WMAUDIO3:
        if ( pImpl->mSeekTable && pImpl->mSeekCount > 0 )
        {
            return pImpl->mSeekTable[ pImpl->mSeekCount - 1 ] / uint32_t( 2 * pImpl->mWaveFormat->nChannels );
        }
        break;

#endif

#if defined(_XBOX_ONE) && defined(_TITLE)

    case WAVE_FORMAT_XMA2:
        return reinterpret_cast<const XMA2WAVEFORMATEX*>( pImpl->mWaveFormat )->SamplesEncoded;

#endif

    default:
        if ( pImpl->mWaveFormat->wBitsPerSample > 0 )
        {
            return static_cast<size_t>( ( uint64_t( pImpl->mAudioBytes ) * 8 )
                                        / uint64_t( pImpl->mWaveFormat->wBitsPerSample * pImpl->mWaveFormat->nChannels ) );
        }
    }

    return 0;
}


size_t SoundEffect::GetSampleDurationMS() const
{
    if ( !pImpl->mWaveFormat || !pImpl->mWaveFormat->nSamplesPerSec )
        return 0;

    uint64_t samples = GetSampleDuration();
    return static_cast<size_t>( ( samples * 1000 ) / pImpl->mWaveFormat->nSamplesPerSec );
}


const WAVEFORMATEX* SoundEffect::GetFormat() const
{
    return pImpl->mWaveFormat;
}


#if defined(_XBOX_ONE) || (_WIN32_WINNT < _WIN32_WINNT_WIN8)

bool SoundEffect::FillSubmitBuffer( _Out_ XAUDIO2_BUFFER& buffer, _Out_ XAUDIO2_BUFFER_WMA& wmaBuffer ) const
{
    memset( &buffer, 0, sizeof(buffer) );
    memset( &wmaBuffer, 0, sizeof(wmaBuffer) );

    buffer.AudioBytes = pImpl->mAudioBytes;
    buffer.pAudioData = pImpl->mStartAudio;
    buffer.LoopBegin = pImpl->mLoopStart;
    buffer.LoopLength = pImpl->mLoopLength;

    uint32_t tag = GetFormatTag( pImpl->mWaveFormat );
    if ( tag == WAVE_FORMAT_WMAUDIO2 || tag == WAVE_FORMAT_WMAUDIO3 )
    {
        wmaBuffer.PacketCount = pImpl->mSeekCount;
        wmaBuffer.pDecodedPacketCumulativeBytes = pImpl->mSeekTable;
        return true;
    }

    return false;
}

#else

void SoundEffect::FillSubmitBuffer( _Out_ XAUDIO2_BUFFER& buffer ) const
{
    memset( &buffer, 0, sizeof(buffer) );
    buffer.AudioBytes = pImpl->mAudioBytes;
    buffer.pAudioData = pImpl->mStartAudio;
    buffer.LoopBegin = pImpl->mLoopStart;
    buffer.LoopLength = pImpl->mLoopLength;
}

#endif
